﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using DirectZZT.Shared.Core;
using DirectZZT.Shared.Scripting;
using DirectZZT.Shared.World;
using DirectZZT.Shared.World.Objects;

#endregion

namespace ZZTWorldSerializer
{
    /// <summary>
    /// Provides the world serializer which serializes "original ZZT" worlds.
    /// </summary>
    [Serializable()]
    [Export("ZZTWorldSerializer", Type = typeof(IWorldSerializer))]
    public class ZZTWorldSerializer : IWorldSerializer
    {
        #region Fields

        /// <summary>
        /// Maps ZZT-element ids to DirectZZT-BoardObject Identifiers.
        /// </summary>
        private Dictionary<byte, Element> _elementMapping;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the ZZTWorldSerializer class.
        /// </summary>
        public ZZTWorldSerializer()
        {
            _elementMapping = new Dictionary<byte, Element>(54);
            // generic elements
            _elementMapping.Add(0, new Element("", ""));
            _elementMapping.Add(1, new Element("Board edge", ""));
            _elementMapping.Add(2, new Element("Messenger", ""));   // < required?

            // player elements
            _elementMapping.Add(3, new Element("Monitor", ""));     // < required?
            _elementMapping.Add(4, new Element("Player", ""));

            // item elements
            _elementMapping.Add(5, new Element("Ammo", ""));
            _elementMapping.Add(6, new Element("Torch", ""));
            _elementMapping.Add(7, new Element("Gem", ""));
            _elementMapping.Add(8, new Element("Key", ""));
            _elementMapping.Add(9, new Element("Door", ""));
            _elementMapping.Add(10, new Element("Scroll", ""));
            _elementMapping.Add(11, new Element("Passage", ""));
            _elementMapping.Add(12, new Element("Duplicator", ""));
            _elementMapping.Add(13, new Element("Bomb", ""));
            _elementMapping.Add(14, new Element("Energizer", ""));
            _elementMapping.Add(15, new Element("Star", ""));
            _elementMapping.Add(16, new Element("Conveyor", "Clockwise"));
            _elementMapping.Add(17, new Element("Conveyor", "CounterClockwise"));
            _elementMapping.Add(18, new Element("Bullet", ""));
            _elementMapping.Add(19, new Element("Water", ""));
            _elementMapping.Add(20, new Element("Forest", ""));
            _elementMapping.Add(21, new Element("Wall", "Solid"));
            _elementMapping.Add(22, new Element("Wall", "Normal"));
            _elementMapping.Add(23, new Element("Wall", "Breakable"));
            _elementMapping.Add(24, new Element("Boulder", ""));
            _elementMapping.Add(25, new Element("Slider", "North-South"));
            _elementMapping.Add(26, new Element("Slider", "East-West"));
            _elementMapping.Add(27, new Element("Wall", "Fake"));
            _elementMapping.Add(28, new Element("Wall", "Invisible"));
            _elementMapping.Add(29, new Element("Wall", "Blink"));
            _elementMapping.Add(30, new Element("Transporter", ""));
            _elementMapping.Add(31, new Element("Line Wall", ""));
            _elementMapping.Add(32, new Element("Ricochet", ""));
            _elementMapping.Add(33, new Element("Blink Ray", "Horizontal"));
            _elementMapping.Add(34, new Element("Bear", ""));
            _elementMapping.Add(35, new Element("Ruffian", ""));
            _elementMapping.Add(36, new Element("Object", ""));
            _elementMapping.Add(37, new Element("Slime", ""));
            _elementMapping.Add(38, new Element("Shark", ""));
            _elementMapping.Add(39, new Element("Spinning Gun", ""));
            _elementMapping.Add(40, new Element("Pusher", ""));
            _elementMapping.Add(41, new Element("Lion", ""));
            _elementMapping.Add(42, new Element("Tiger", ""));
            _elementMapping.Add(43, new Element("Blink Ray", "Vertical"));
            _elementMapping.Add(44, new Element("Centipede", "Head"));
            _elementMapping.Add(45, new Element("Centipede", "Segment"));
            _elementMapping.Add(47, new Element("Text", "Blue"));
            _elementMapping.Add(48, new Element("Text", "Green"));
            _elementMapping.Add(49, new Element("Text", "Cyan"));
            _elementMapping.Add(50, new Element("Text", "Red"));
            _elementMapping.Add(51, new Element("Text", "Purple"));
            _elementMapping.Add(52, new Element("Text", "Brown"));
            _elementMapping.Add(53, new Element("Text", "Black"));

            // fetch all concrete types now
            ExportedType[] exports = TypeManager.Instance.GetExports();
            foreach (var item in _elementMapping)
            {
                var m = exports.FirstOrDefault(t =>
                {
                    BoardObjectAttribute attribute = BoardObjectAttribute.GetAttribute(t.Type);
                    return (attribute != null && attribute.Identifier == item.Value.Identifier);
                });
                if (m.Type != null)
                {
                    item.Value.Type = m.Type.AssemblyQualifiedName;
                }
            }
        }

        #endregion

        #region IWorldSerializer Members

        string IWorldSerializer.Extension
        {
            get { return "zzt"; }
        }

        string IWorldSerializer.SaveExtension
        {
            get { return "sav"; }
        }

        bool IWorldSerializer.IsValid(Stream stream)
        {
            using (BinaryReader reader = new BinaryReader(stream))
            {
                // expect 0xFF and 0xFF for the first two bytes in the stream, then we're ok
                return ((reader.ReadByte() == (byte)255)
                    || (reader.ReadByte() == (byte)255));
            }
        }

        void IWorldSerializer.Load(Stream stream, World destination, WorldDeserializeOptions options)
        {
            destination.FeatureSet = FeatureSet.ZZT;

            // that's gonna be dirty...
            using (BinaryReader reader = new BinaryReader(stream, Encoding.Default))
            {
                // file format: http://nanobox.chipx86.com/mwenc/fileformat.html or PDF located in "Resources/" directory

                // byte 0 - 1
                // expect magic number 255 for the first two bytes
                if ((reader.ReadByte() != (byte)255)
                    || (reader.ReadByte() != (byte)255))
                {
                    return;
                }

                // byte 2 - 3 (required later), increment by one (title screen isn't counted in here)
                ushort numBoards = (ushort)(reader.ReadUInt16() + 1);

                // byte 4 - 5
                destination.Data.Player.AmmoAmount = (int)reader.ReadUInt16();

                // byte 6 - 7
                ushort numGems = reader.ReadUInt16();

                // byte 8 - Blue Key
                destination.Data.Player.HasBlueKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 9 - Green Key
                destination.Data.Player.HasGreenKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 10 - Cyan Key
                destination.Data.Player.HasCyanKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 11 - Red Key
                destination.Data.Player.HasRedKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 12 - Purple Key
                destination.Data.Player.HasPurpleKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 13 - Yellow Key
                destination.Data.Player.HasYellowKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 14 - White Key
                destination.Data.Player.HasWhiteKey = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 15 - 16
                destination.Data.Player.Health = (int)reader.ReadInt16();

                // byte 17 - 18
                destination.InitialBoard = (int)reader.ReadInt16();

                // byte 19 - 20
                destination.Data.Player.Torches = reader.ReadInt16();

                // byte 21 - 22
                destination.Data.Player.TorchCycles = reader.ReadInt16();

                // byte 23 - 24
                destination.Data.Player.EnergizerCycles = reader.ReadInt16();

                // byte 25 - 26 (padding)
                reader.ReadByte();
                reader.ReadByte();

                // byte 27 - 28
                ushort score = reader.ReadUInt16();

                // byte 29 - GNL
                byte gameNameLength = reader.ReadByte();

                // byte 30 - 50 - game name/destination name
                string gameName = "";
                for (int i = 0; i < 20; i++)
                {
                    char c = (char)reader.ReadByte();
                    if (i < gameNameLength)
                    {
                        gameName += c;
                    }
                }
                destination.Title = gameName;

                // byte 51 - 259 - read flags
                for (int flagIndex = 0; flagIndex < 10; flagIndex++)
                {
                    byte flagLength = reader.ReadByte();
                    string flagName = "";

                    for (int i = 0; i < 20; i++)
                    {
                        char c = (char)reader.ReadByte();
                        if (i < flagLength)
                        {
                            flagName += c;
                        }
                    }

                    // add to flags
                    destination.Data.Flags[flagName] = ScriptFlag.Empty;
                }

                // byte 260 - 261 - time passed
                destination.Data.TimePassed = reader.ReadInt16();

                // byte 262 - 263 - two bytes (padding)
                reader.ReadByte();
                reader.ReadByte();

                // byte 264 - save byte
                bool saveByte = (reader.ReadByte() == (byte)1 ? true : false);

                // byte 247 - ... - padding
                for (int i = 0; i < 247; i++)
                {
                    reader.ReadByte();
                }

                // byte 512 onwards - iterate over all boards
                for (short boardIndex = 0; boardIndex < numBoards; boardIndex++)
                {
                    // create board
                    Board board = new Board();

                    // it is an actual UInt16, but it's proposed as SINT16
                    ushort boardSizeBytes = reader.ReadUInt16();

                    // board title/ length
                    byte boardTitleLength = reader.ReadByte();
                    string boardTitle = "";
                    for (int i = 0; i < 20; i++)
                    {
                        char c = (char)reader.ReadByte();
                        if (i < boardTitleLength)
                        {
                            boardTitle += c;
                        }
                    }
                    board.BoardId = boardIndex;
                    board.Title = boardTitle;
                    destination.AddBoard(board);

                    // padding
                    for (int i = 0; i < 9; i++)
                    {
                        reader.ReadByte();
                    }

                    // RLE (run-length encoded tile data)
                    // overall index for iteration
                    int index = 0;
                    // hold temporary variables
                    int count = 0;
                    byte element = 0;
                    byte color = 0;
                    // iterate over the whole playfield
                    while (index < Board.BoardWidth * Board.BoardHeight)
                    {
                        if (count == 0)
                        {
                            count = reader.ReadByte();
                            element = reader.ReadByte();
                            color = reader.ReadByte();

                            // re-format: 0 means 256 (wrapped)
                            if (count == 0) { count = 256; }
                        }

                        // ignore emptyness
                        if (element != 0)
                        {
                            BoardObject obj = GetFromElement(element, color);
                            if (obj != null)
                            {
                                obj.X = index % Board.BoardWidth;
                                obj.Y = (int)Math.Floor((double)(index / Board.BoardWidth));
                                obj.SetColor(color);
                                board.SpawnObject(obj, null);
                            }
                            else
                            {
                                //Logging.GetLogging().LogFormat(LoggingMessageType.Warning, "Serializer", "", "Element with Identifier '{0}' at {1}, {2} could not be constructed!", element, x, y);
                            }
                        }

                        // decrement current run count
                        count--;
                        // increment overall index
                        index++;
                    }

                    destination.Data.Player.ShotsAmount = (int)reader.ReadByte();
                    board.IsDark = (reader.ReadByte() == (byte)1 ? true : false);
                    board.BoardNorthIndex = (int)reader.ReadByte();
                    board.BoardSouthIndex = (int)reader.ReadByte();
                    board.BoardWestIndex = (int)reader.ReadByte();
                    board.BoardEastIndex = (int)reader.ReadByte();
                    board.ReEnterWhenZapped = (reader.ReadByte() == (byte)1 ? true : false);

                    // message
                    byte msgLen = reader.ReadByte();
                    string message = "";
                    for (int i = 0; i < 33; i++)
                    {
                        char c = (char)reader.ReadByte();
                        if (i < boardTitleLength)
                        {
                            message += c;
                        }
                    }

                    // padding
                    reader.ReadByte();
                    reader.ReadByte();

                    // time limit
                    board.TimeLimit = reader.ReadUInt16();

                    // padding (9 bytes)
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();
                    reader.ReadByte();

                    ushort numObjects = reader.ReadUInt16();

                    // go to next board
                }
            }
        }

        void IWorldSerializer.Save(World world, Stream stream, WorldSerializeOptions options)
        {

        }

        #endregion

        #region Methods

        private BoardObject GetFromElement(byte element, int color, params string[] parameters)
        {
            if (_elementMapping.ContainsKey(element) && !string.IsNullOrWhiteSpace(_elementMapping[element].Type))
            {
                Type type = Type.GetType(_elementMapping[element].Type);
                if (type != null)
                {
                    return (BoardObject)Activator.CreateInstance(type);
                }
            }
            return null;
        }

        #endregion

        #region Nested types

        /// <summary>
        /// Specifies one element which will be a BoardObject later.
        /// </summary>
        class Element
        {
            /// <summary>
            /// Identifier of the element.
            /// </summary>
            public string Identifier;
            /// <summary>
            /// Optional parameters for the element.
            /// </summary>
            public string Modifiers;
            /// <summary>
            /// The type.
            /// </summary>
            public string Type;

            /// <summary>
            /// 
            /// </summary>
            /// <param name="identifier"></param>
            /// <param name="modifiers"></param>
            public Element(string identifier, string modifiers)
            {
                this.Identifier = identifier;
                this.Modifiers = modifiers;
                this.Type = null;
            }
        }

        #endregion
    }
}
